package com.ictravi.iamunit.connector;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.idmunit.IdMUnitException;

import com.ictravi.iamunit.IAMUnitException;
import com.ictravi.iamunit.IAMUnitFailureException;
import com.ictravi.iamunit.IAMUnitTestCase;
import com.ictravi.iamunit.Failures;

/**
 * LDAPConnector
 * 
 * @author Albert Louw (alouw@ictavi.com)
 * @version v1.0.0
 **/
public class LDAPConnector extends AbstractConnector {
	private final static String STR_UNICODE_PASSWORD = "unicodePwd";
	protected final static String STR_DXML_ASSOC = "DirXML-Associations";
	private final static String STR_DN = "dn";
	private final static String STR_NEW_DN = "newdn";
	private final static String STR_USER_PASSWORD = "userPassword";
	protected final static String STR_SUCCESS = "...SUCCESS";

	private boolean insensitive = false;

	private static Log LOG = LogFactory.getLog(IAMUnitTestCase.class);
	private TreeSet<String> operationalAttributes = new TreeSet<String>(
			String.CASE_INSENSITIVE_ORDER);
	private Map<String, String> config;
	private DirContext m_context;

	/**
	 * getter for DDSteps for OperationalDataMap
	 * 
	 * @since v1.0.0
	 * @param config
	 *            configuration setting
	 * @throws IAMUnitException
	 **/
	public void setup(Map<String, String> config) throws IAMUnitException {
		this.config = config;
		LOG.debug("...in: LDAPConnector");
		this.m_context = createLDAPConnection();
	}

    public void opModObject(Map<String, Collection<String>> data) throws IAMUnitException {
        opReplaceAttr(data);
    }

    public void opModifyObject(Map<String, Collection<String>> data) throws IAMUnitException {
        opReplaceAttr(data);
    }

	/**
	 * opAddAttr
	 * 
	 * @since v1.0.0
	 * @param dataRow
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opAddAttr(Map<String, Collection<String>> dataRow)
			throws IAMUnitException {
		modifyObject(dataRow, DirContext.ADD_ATTRIBUTE);
		LOG.info(STR_SUCCESS);
	}

	/**
	 * opAddObject
	 * 
	 * @since v1.0.0
	 * @param dataRow
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opAddObject(Map<String, Collection<String>> dataRow)
			throws IAMUnitException {
		String dn = null;
		Attributes createAttrs = new BasicAttributes();
		// insert attributes from incoming map
		for (String name : dataRow.keySet()) {
			Collection<String> dataValue = dataRow.get(name);

			if (dataValue.size() == 0) {
				continue;
			}

			if (name.equalsIgnoreCase(STR_DN)) {
				dn = dataValue.iterator().next();
			} else if (name.equalsIgnoreCase(STR_UNICODE_PASSWORD)) {
				byte[] unicodePwdVal = getUnicodeBytes(dataValue.iterator()
						.next());
				createAttrs.put(name, unicodePwdVal);
			} else {
				BasicAttribute multiValuedAttr = new BasicAttribute(name);
				for (Iterator<String> i = dataValue.iterator(); i.hasNext();) {
					multiValuedAttr.add(i.next());
				}
				createAttrs.put(multiValuedAttr);
			}
		}

		if (dn == null) {
			throw new IAMUnitException(
					"A Distinguished Name must be supplied in column '"
							+ STR_DN + "'");
		}

		LOG.debug("Binding DN " + dn);
		try {
			DirContext tmpCtx = this.m_context
					.createSubcontext(dn, createAttrs);
			if (tmpCtx != null) {
				tmpCtx.close();// this is necessary in order to keep the parent
								// connection ctx clean enough to be
								// pooled/managed as week references inside of
								// the parent DirContext will prevent proper
								// pooling
			}
			LOG.debug(">>>" + dn + " created");
		} catch (NamingException e) {
			throw new IAMUnitException("Failed to create object: " + dn
					+ " with error: " + e.getMessage(), e);
		}
		LOG.info(STR_SUCCESS);
	}

	/**
	 * opClearAttr
	 * 
	 * @since v1.0.0
	 * @param data
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opClearAttr(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String dn = getTargetDn(data);
		LOG.debug("...performing LDAP modification for: [" + dn + "]");

		List<ModificationItem> mods = new ArrayList<ModificationItem>();
		for (String attrName : data.keySet()) {
			if (attrName.equalsIgnoreCase(STR_DN)) {
				continue;
			}

			for (String attrVal : data.get(attrName)) {
				if (attrVal.equals("*") == false) {
					throw new IAMUnitException(
							"You must specify '*' as the attribute value for the clearAttr operation.");
				}
			}
			mods.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
					new BasicAttribute(attrName)));
		}

		if (mods.size() > 0) {
			try {
				m_context.modifyAttributes(dn, (ModificationItem[]) mods
						.toArray(new ModificationItem[mods.size()]));
			} catch (NameNotFoundException e) {
				LOG.warn("...WARNING: object doesn't exist, continuing.");
				// TODO: send warning here?
			} catch (NamingException e) {
				if (e.getMessage().contains("16")) {
					LOG.warn("...already removed, operation unnecessary.");
				} else {
					throw new IAMUnitException("Modification failure: Error: "
							+ e.getMessage(), e);
				}
			}
		} else {
			LOG.debug("...No attributes to update");
		}
		LOG.info(STR_SUCCESS);
	}

	/**
	 * opDeleteObject
	 * 
	 * @since v1.0.0
	 * @param data
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opDeleteObject(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String dn;
		try {
			dn = getTargetDn(data);
		} catch (IAMUnitException e) {
			// A wild-card search failed to find an entry, or the DN field in
			// the spreadsheet is blank
			LOG.warn("---> Wild-card deletion found no DNs matching the specified filter, or the DN in the spreadsheet is blank.");
			return; // Never fail when told to delete a DN that isn't there
		}

		try {
			m_context.lookup(dn);
			LOG.info("...performing LDAP deletion for: [" + dn + "]");
			m_context.unbind(dn);
			LOG.info(STR_SUCCESS);
		} catch (NamingException e) {
			String errorMessage = e.getMessage().toUpperCase();
			if (errorMessage.indexOf("NO SUCH ENTRY") != -1
					|| errorMessage.indexOf("NO_OBJECT") != -1
					|| errorMessage.indexOf("OBJECT") != -1) {
				return;
			} else {
				throw new IAMUnitException("Deletion failure: Invalid DN: "
						+ e.getMessage());
			}
		}
	}

	/**
	 * opDeleteMatching
	 * 
	 * @since v1.0.0
	 * @param data
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
    public void opDeleteMatching(Map<String, Collection<String>> data) throws IAMUnitException {
        String dn;
        LOG.info("...performing LDAP deletion for objects matching.");
        try {
        	dn = getTargetDn(data);
            while (dn.length() > 0)
            {
                try {
                    m_context.lookup(dn);
                    LOG.info("...performing LDAP deletion for: [" + dn + "]");
                    m_context.unbind(dn);
                    LOG.info(STR_SUCCESS);
                    dn = "";
                } catch (NamingException e) {
                    String errorMessage = e.getMessage().toUpperCase();
                    if(errorMessage.indexOf("NO SUCH ENTRY")!= -1 || errorMessage.indexOf("NO_OBJECT")!=-1 || errorMessage.indexOf("OBJECT")!=-1) {
                        return;
                    } else {
                        throw new IAMUnitException("Deletion failure: Invalid DN: " + e.getMessage());
                    }
                }
            	dn = getTargetDn(data);
            }
        } catch (IAMUnitException e) {
            //A wild-card search failed to find an entry, or the DN field in the spreadsheet is blank
            LOG.info("...No (More) objects found for deletion.");
            return; //Never fail when told to delete a DN that isn't there
        }
    }
    
	/**
	 * opMoveObject
	 * 
	 * @since v1.0.0
	 * @param data
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opMoveObject(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String dn = getSingleValue(data, STR_DN);
		if (dn == null) {
			throw new IAMUnitException(
					"A Distinguished Name must be supplied in column '"
							+ STR_DN + "'");
		}

		String newDn = getSingleValue(data, STR_NEW_DN);
		if (newDn == null) {
			throw new IAMUnitException(
					"A Distinguished Name must be supplied in column '"
							+ STR_NEW_DN + "'");
		}

		LOG.info("...performing LDAP move/rename for: [" + dn + "] to ["
				+ newDn + "].");
		try {
			m_context.rename(dn, newDn);
			LOG.info(STR_SUCCESS);
		} catch (NamingException e) {
			throw new IAMUnitException("Move/Rename failure: Error: "
					+ e.getMessage(), e);
		}
		LOG.info(STR_SUCCESS);
	}

	/**
	 * opRemoveAttr
	 * 
	 * @since v1.0.0
	 * @param data
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opRemoveAttr(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String dn = getTargetDn(data);
		LOG.info("...performing LDAP modification for: [" + dn + "]");

		TreeMap<String, Collection<String>> curAttrs;
		try {
			curAttrs = getAttributes(dn);
		} catch (IAMUnitException e) {
			LOG.info(e.toString());
			return;
		}

		List<ModificationItem> mods = new ArrayList<ModificationItem>();
		for (String attrName : data.keySet()) {
			if (attrName.equalsIgnoreCase(STR_DN)) {
				continue;
			} else if (attrName.equals(STR_UNICODE_PASSWORD)) {
				byte[] unicodePwdVal = getUnicodeBytes(getSingleValue(data,
						STR_UNICODE_PASSWORD));
				mods.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
						new BasicAttribute(attrName, unicodePwdVal)));
			} else if (attrName.equals(STR_DXML_ASSOC)) {
				Collection<String> values = data.get(attrName);
				Collection<String> curAssociations = curAttrs
						.get(STR_DXML_ASSOC);
				for (String attrVal : values) {
					String[] association = attrVal.split("#", 3);

					String oldAttrVal = getDXMLAssocByDriverName(
							association[0], curAssociations);
					if (oldAttrVal == null) {
						throw new IAMUnitException(
								"Failed to update attribute ["
										+ STR_DXML_ASSOC
										+ "] because a value matching the given driver DN was not found in the target object.");
					}
					assert (oldAttrVal.length() > 0);

					mods.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
							new BasicAttribute(attrName, oldAttrVal)));
				}
			} else {
				Collection<String> values = data.get(attrName);
				Attribute modValues = new BasicAttribute(attrName);

				Collection<String> curValues = curAttrs.get(attrName);
				// Only remove values if they are actually present on the object
				if (curValues != null) {
					for (String curValue : curValues) {
						for (String value : values) {
							// TODO: Add configuration option at connector
							// definition level to enable/disable case
							// sensitivity for rex-ex comparisons
							if (curValue.matches(value)) {
								modValues.add(curValue);
							}
						}
					}
				}
				// Only apply the modification if there are explicit values to
				// remove, otherwise all values will be removed!
				if (modValues.size() > 0) {
					mods.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
							modValues));
					LOG.debug("...preparing to remove [" + modValues
							+ "] from '" + attrName + "'.");
				} else {
					LOG.warn("...WARNING: specified value doesn't exist, continuing.");
				}
			}
		}

		if (mods.size() > 0) {
			try {
				m_context.modifyAttributes(dn, (ModificationItem[]) mods
						.toArray(new ModificationItem[mods.size()]));
			} catch (NamingException e) {
				if (e.getMessage().contains("16")) {
					LOG.warn("...already removed, operation unnecessary.");
				} else {
					throw new IAMUnitException("Modification failure: Error: "
							+ e.getMessage(), e);
				}
			}
		} else {
			LOG.debug("...No attributes to update");
		}

		LOG.info(STR_SUCCESS);
	}

	/**
	 * opRenameObject
	 * 
	 * @since v1.0.0
	 * @param assertedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opRenameObject(Map<String, Collection<String>> assertedAttrs)
			throws IAMUnitException {
		opMoveObject(assertedAttrs);
		LOG.info(STR_SUCCESS);
	}

	/**
	 * opReplaceAttr
	 * 
	 * @since v1.0.0
	 * @param dataRow
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opReplaceAttr(Map<String, Collection<String>> dataRow)
			throws IAMUnitException {
		modifyObject(dataRow, DirContext.REPLACE_ATTRIBUTE);
	}

	/**
	 * opAttrDoesNotExist
	 * 
	 * @since v1.0.0
	 * @param expectedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opAttrDoesNotExist(Map<String, Collection<String>> expectedAttrs)
			throws IAMUnitException {
		doValidate(expectedAttrs, true);
	}

	/**
	 * opValidateObject
	 * 
	 * @since v1.0.0
	 * @param expectedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opValidateObject(Map<String, Collection<String>> expectedAttrs)
			throws IAMUnitException {
		doValidate(expectedAttrs, false);
		LOG.info("...SUCCESS");
	}

	/**
	 * doValidate
	 * 
	 * @since v1.0.0
	 * @param expectedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void doValidate(Map<String, Collection<String>> expectedAttrs,
			boolean bIsAttrDoesNotExistTest) throws IAMUnitException {
		String dn = getTargetDn(expectedAttrs);

		Map<String, Collection<String>> appAttrs = getAttributes(dn);

		Failures failures = new Failures();
		for (String attrName : expectedAttrs.keySet()) {
			if (attrName.equalsIgnoreCase(STR_DN)) {
				continue;
			}

			Collection<String> expectedValues = expectedAttrs.get(attrName);
			Collection<String> actualValues;
			if (operationalAttributes.contains(attrName)) {
				// Operational attributes are not returned from the server when
				// all attributes are requested so we need to read them
				// explicitly.
				actualValues = getAttributes(dn, new String[] { attrName })
						.get(attrName);
				continue;
			} else {
				actualValues = appAttrs.get(attrName);
			}

			if (actualValues == null || actualValues.size() == 0) {
				if (bIsAttrDoesNotExistTest) {
					return;
				}
				failures.add("Validation failed: Attribute ["
						+ attrName
						+ "] not equal.  Expected value: ["
						+ expectedValues
						+ "] but the attribute value did not exist in the application.");
			} else {
				compareAttributeValues(attrName, expectedValues, actualValues,
						failures);
			}
		}

		if (failures.hasFailures()) {
			throw new IAMUnitFailureException(failures.toString());
		}
	}

	/**
	 * opValidatePassword
	 * 
	 * @since v1.0.0
	 * @param expectedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	public void opValidatePassword(Map<String, Collection<String>> expectedAttrs)
			throws IAMUnitException {
		InitialDirContext tempConn = null;
		try {
			String dn = getTargetDn(expectedAttrs);
			String passwordVal = getSingleValue(expectedAttrs,
					STR_USER_PASSWORD);
			LOG.info("...performing LDAP password validation for: [" + dn + "]");
			tempConn = createLDAPConnection(dn, passwordVal);
			LOG.info(STR_SUCCESS);
		} catch (IAMUnitException e) {
			throw new IAMUnitFailureException(
					"Password validation failure: Error: " + e.getMessage());
		} finally {
			if (tempConn != null) {
				try {
					tempConn.close();
				} catch (NamingException e) {
					LOG.warn("...Failed to close validatePassword context.");
				}
			}
		}
		LOG.info(STR_SUCCESS);
	}

	/**
	 * tears down connection
	 * 
	 * @since v1.0.0
	 * @throws IAMUnitException
	 **/
	public void tearDown() throws IAMUnitException {
		LOG.debug("...in LDAPConnector - tearDown");
		try {
			if (m_context != null) {
				this.m_context.close();
				this.m_context = null;
			}
		} catch (NamingException e) {
			throw new IAMUnitException("Failed to close ldap connection: "
					+ e.getMessage());
		}
	}

	/**
	 * opValidatePassword
	 * 
	 * @since v1.0.0
	 * @param expectedAttrs
	 *            operational attributes
	 * @throws IAMUnitException
	 **/
	private InitialDirContext createLDAPConnection() throws IAMUnitException {
		String userDN = config.get("username");
		String password = config.get("password");

		return createLDAPConnection(userDN, password);
	}

	/**
	 * createLDAPConnection
	 * 
	 * @since v1.0.0
	 * @param userDN
	 *            username
	 * @param password
	 * 			  password for the username
	 * @throws IAMUnitException
	 **/
	private InitialDirContext createLDAPConnection(String userDN,
			String password) throws IAMUnitException {
		String server = config.get("server");
		String port = ":" + config.get("port");
		if (port.length() == 1)
			port = "";
		String keystorePath = config.get("keystore-path");

		Hashtable<String, String> env = new Hashtable<String, String>();
		if (keystorePath != null && keystorePath.length() > 0) {
			System.setProperty("javax.net.ssl.trustStore", keystorePath);
			env.put(Context.SECURITY_PROTOCOL, "ssl");
		}
		env.put(Context.INITIAL_CONTEXT_FACTORY,
				"com.sun.jndi.ldap.LdapCtxFactory");
		env.put("com.sun.jndi.ldap.connect.pool", "true");
		env.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl");
		env.put("com.sun.jndi.ldap.connect.pool.timeout", "1000");
		env.put("com.sun.jndi.ldap.connect.pool.maxsize", "3");
		env.put("com.sun.jndi.ldap.connect.pool.prefsize", "1");
		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		env.put(Context.PROVIDER_URL, "ldap://" + server + port);
		env.put(Context.SECURITY_PRINCIPAL, userDN);
		env.put(Context.SECURITY_CREDENTIALS, password);
		env.put("com.sun.jndi.ldap.connect.timeout", "5000");
		env.put(Context.REFERRAL, "follow");

		try {
			return new InitialDirContext(env);
		} catch (Exception e) {
			if (keystorePath != null && keystorePath.length() > 0) {
				LOG.debug("### Failed to obtain an SSL LDAP server connection to: ["
						+ server + "].");
				throw new IAMUnitException(
						"Failed to obtain an SSL LDAP Connection: "
								+ e.getMessage(), e);
			} else {
				LOG.debug("### Failed to obtain an LDAP server connection to: ["
						+ server + "].");
				throw new IAMUnitException(
						"Failed to obtain an LDAP Connection: "
								+ e.getMessage());
			}
		}
	}

	/**
	 * createLDAPConnection
	 * 
	 * @since v1.0.0
	 * @param base
	 *            Base container
	 * @param filter
	 * 			  LDAP Filter
	 * @param scope
	 *            Search scope (ONELEVEL, SUBTREE, OBJECT)
	 * @return String
	 * @throws IAMUnitException
	 **/
	private String findUser(String base, String filter, String scope) throws IAMUnitException {
		String resolvedDn = null;

		SearchControls ctls = new SearchControls();
        ctls.setCountLimit(1); //TEST
        if (scope.equalsIgnoreCase("ONELEVEL"))
   		{
            ctls.setSearchScope(SearchControls.ONELEVEL_SCOPE); //TEST
  		}
        else
        	if (scope.equalsIgnoreCase("OBJECT"))
        	{
        		ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
        	}
        	else
        		ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);

		try {
			// Find users with a matching ID
			NamingEnumeration<SearchResult> results = this.m_context.search(
					base, filter, ctls);
			SearchResult sr;
			int resultCtr = 0;
			while (results.hasMoreElements()) {
				++resultCtr;
				sr = (SearchResult) results.next();
				if (resultCtr == 1) {
					if (sr.getName() != null && sr.getName().length() > 0) {
						resolvedDn = sr.getName() + "," + base;
					} else {
						resolvedDn = base;
					}
					LOG.debug("---> Target DN for validation: [" + resolvedDn
							+ "]");
				} else {
					LOG.debug("---> Other objects found matching filter: ["
							+ sr.getName() + "," + base + "].");
				}
			}
		} catch (NamingException ne) {
			throw new IAMUnitException("Object Lookup for filter [" + filter
					+ "], base [" + base + "] Failed: " + ne.getMessage());
		}
		LOG.info("...found user: " + resolvedDn);
		return resolvedDn;
	}

	/**
	 * getAttributes
	 * 
	 * @since v1.0.0
	 * @param dn
	 *            dn to read
	 * @return Treemap
	 * @throws IAMUnitException
	 **/
	private TreeMap<String, Collection<String>> getAttributes(String dn)
			throws IAMUnitException {
		try {
			DirContext tmp = (DirContext) m_context.lookup(dn);
			if (tmp != null) {
				tmp.close(); // this is necessary in order to keep the parent
								// connection ctx clean enough to be
								// pooled/managed as week references inside of
								// the parent DirContext will prevent proper
								// pooling
			}
		} catch (NameNotFoundException e) {
			throw new IAMUnitException("Could not find object: [" + dn
					+ "] to retrieve attributes.", e);
		} catch (NamingException e) {
			throw new IAMUnitException("Error resolving '" + dn + "'.", e);
		}

		try {
			return attributesToMap(m_context.getAttributes(dn));
		} catch (NamingException e) {
			throw new IAMUnitException("Error reading attributes for '" + dn
					+ "'.", e);
		}
	}

	private TreeMap<String, Collection<String>> getAttributes(String dn,
			String[] attrs) throws IAMUnitException {
		try {
			Attributes operationalAttrs = m_context.getAttributes(dn, attrs);
			return attributesToMap(operationalAttrs);
		} catch (NamingException e) {
			throw new IAMUnitException("Error reading attributes for '" + dn
					+ "'.");
		}
	}

	protected String getTargetDn(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String STR_BASE_DN_DELIMITER = ",base=";

		String dn = getSingleValue(data, STR_DN);

		if (!dn.trim().equalsIgnoreCase(dn)) {
			throw new IAMUnitException(
					"WARNING: your DN specified: ["
							+ dn
							+ "] is either prefixed or postfixed with whitespace!  Please correct, then retest.");
		}

		if (dn.toString().length() == 0) {
			throw new IAMUnitException(
					"A Distinguished Name must be supplied in column '"
							+ STR_DN + "'");
		}

		// Detect LDAP filter in the DN
		if (dn.startsWith("(")) {
			// Search for the user by a standard LDAP filter
			// The format for this field is "<filter>,base=<container dn>"
			int startOfBase = dn.indexOf(STR_BASE_DN_DELIMITER);
			if (startOfBase == -1) {
				throw new IAMUnitException(
						"Check the dn or LDAP filter specified in the spreadsheet. Should be listed in the form (LDAPFilter),base=LDAPSearchBase.  Example: (&(objectClass=inetOrgPerson)(cn=testuser1)),base=o=users.");
			}

			String filter = dn.substring(0, startOfBase);
			String base = dn.substring(startOfBase
					+ STR_BASE_DN_DELIMITER.length());

			dn = findUser(base, filter,"ONELEVEL");
		} else {
			// Detect standard wildcard token * in the ID
			String[] nameComponents = dn.split("(?<!\\\\),");
			String idVal = nameComponents[0];
			if (idVal.indexOf("*") == -1)
				return dn;
			// cn=TIDMTST*1,ou=users,o=myorg
			LOG.debug("---> ID to search: " + idVal);

			String base = dn.substring(dn.indexOf(nameComponents[1]));
			String filter = "(" + idVal + ")";
			LOG.debug("---> Synthesized filter: " + filter + " from the base: "
					+ base);

			dn = findUser(base, filter,"ONELEVEL");
		}

		if (dn == null || dn.length() < 1) {
			throw new IAMUnitException(
					"Failed to resolve target DN: Check the dn or LDAP filter specified in the spreadsheet to ensure it returns results.  Recommended: test the filter in an LDAP browser first.");
		}

		dn = dn.replaceAll("/", "\\\\/");
		dn = dn.replaceAll("\"", "");

		return dn;
	}

	private void modifyObject(Map<String, Collection<String>> dataRow,
			int operationType) throws IAMUnitException {
		String dn = getTargetDn(dataRow);
		LOG.debug("...performing LDAP modification for: [" + dn + "]");

		List<ModificationItem> mods = new ArrayList<ModificationItem>();
		for (String attrName : dataRow.keySet()) {
			if (attrName.equalsIgnoreCase(STR_DN)) {
				continue;
			} else if (attrName.equals(STR_UNICODE_PASSWORD)) {
				byte[] unicodePwdVal = getUnicodeBytes(getSingleValue(dataRow,
						STR_UNICODE_PASSWORD));
				mods.add(new ModificationItem(operationType,
						new BasicAttribute(attrName, unicodePwdVal)));
			} else if (attrName.equals(STR_DXML_ASSOC)
					&& operationType == DirContext.REPLACE_ATTRIBUTE) {
				Collection<String> curAssociations = getAttributes(dn,
						new String[] { STR_DXML_ASSOC }).get(STR_DXML_ASSOC);
				for (String attrVal : dataRow.get(attrName)) {
					String[] association = attrVal.split("#", 3);

					if (curAssociations != null) {
						String oldAttrVal = getDXMLAssocByDriverName(
								association[0], curAssociations);
						if (oldAttrVal != null) {
							assert (oldAttrVal.length() > 0);

							// If the new association doesn't have a the path
							// component (the third component), use the path
							// component from the current association.
							if (association.length == 2) {
								String[] oldAssociation = oldAttrVal.split("#",
										3);
								if (oldAssociation.length == 3) {
									attrVal = association[0] + "#"
											+ association[1] + "#"
											+ oldAssociation[2];
								}
							}

							mods.add(new ModificationItem(
									DirContext.REMOVE_ATTRIBUTE,
									new BasicAttribute(attrName, oldAttrVal)));
						}
					}
					mods.add(new ModificationItem(DirContext.ADD_ATTRIBUTE,
							new BasicAttribute(attrName, attrVal)));
					continue;
				}
			} else {
				Collection<String> values = dataRow.get(attrName);
				Attribute modValues = new BasicAttribute(attrName);
				for (Iterator<String> j = values.iterator(); j.hasNext();) {
					modValues.add(j.next());
				}
				mods.add(new ModificationItem(operationType, modValues));
				LOG.debug("...preparing to update attr: [" + attrName
						+ "] with value [" + values + "]");
			}
		}

		if (mods.size() > 0) {
			try {
				m_context.modifyAttributes(dn, (ModificationItem[]) mods
						.toArray(new ModificationItem[mods.size()]));
			} catch (NamingException e) {
				if (e.getMessage().contains("16")) {
					LOG.warn("...already removed, operation unnecessary.");
				} else {
					throw new IAMUnitException("Modification failure: Error: "
							+ e.getMessage(), e);
				}
			}
		} else {
			LOG.debug("...No attributes to update");
		}

		LOG.info("...SUCCESS");
	}

	@SuppressWarnings("unchecked")
	private static TreeMap<String, Collection<String>> attributesToMap(
			Attributes attributes) throws NamingException {
		TreeMap<String, Collection<String>> attrs = new TreeMap<String, Collection<String>>(
				String.CASE_INSENSITIVE_ORDER);
		NamingEnumeration<Attribute> i = null;
		try {
			for (i = (NamingEnumeration<Attribute>) attributes.getAll(); i
					.hasMore();) {
				Attribute attr = (Attribute) i.next();
				String attrName = attr.getID();
				List<String> attrValues = new LinkedList<String>();
				for (NamingEnumeration<Object> j = (NamingEnumeration<Object>) attr
						.getAll(); j.hasMore();) {
					Object value = j.next();
					if (value instanceof String) {
						attrValues.add((String) value);
					} else {
						LOG.info("Not adding value for '" + attrName
								+ "' because it is not a String.");
					}
				}
				attrs.put(attrName, attrValues);
			}
		} catch (NamingException e) {
			try {
				if (i != null) {
					i.close();
				}
			} catch (NamingException e1) {
				LOG.info("An error occurred closing the NamingEnumeration. '"
						+ e.getMessage() + "'");
			}
			throw e;
		}

		return attrs;
	}

    protected void compareAttributeValues(String attrName, Collection<String> expected, Collection<String> actual, Failures failures) throws IAMUnitException {
        Collection<String> unmatched = new LinkedList<String>(actual);
        outer:
        for (String expectedValue : expected) {
        	Pattern p;
        	for (Iterator<String> i=unmatched.iterator(); i.hasNext(); ) {
        		String actualValue = i.next();
        		if (expectedValue.startsWith("@") == true)
        		{
                    String substr = expectedValue.substring(1,expectedValue.length());                    
                    if (actualValue.contains(substr) == true)
                    {
        				LOG.info(STR_SUCCESS + ": validating contains attribute: [" + attrName + "] EXPECTED: [" + expectedValue +"] ACTUAL: [" + actualValue + "]");
                    }
                    else
                    {
                    	failures.add(attrName,expected,actual);
                    }
                    i.remove();
                    continue outer;
        		}
        		else
        		{
        			if (actualValue.equals(expectedValue))
        			{
        				LOG.info(STR_SUCCESS + ": validating attribute: [" + attrName + "] EXPECTED: [" + expectedValue +"] ACTUAL: [" + actualValue + "]");
        				i.remove();
        				continue outer;        			
        			}
        			else
        			{		
    					p = Pattern.compile(expectedValue, insensitive ? Pattern.CASE_INSENSITIVE : 0);
        				if (p.matcher(actualValue).matches()) 
        				{
        					LOG.info(STR_SUCCESS + ": validating attribute: [" + attrName + "] EXPECTED: [" + expectedValue +"] ACTUAL: [" + actualValue + "]");
        					i.remove();
        					continue outer;
        				}
        			}
        		}
        	}
            failures.add(attrName, expected, actual);
        	return;
        }
    }

	private static String getDXMLAssocByDriverName(String driverDn,
			Collection<String> attr) {
		for (Iterator<String> i = attr.iterator(); i.hasNext();) {
			Object attrVal = i.next();
			if (attrVal != null) {
				if (attrVal instanceof String) {
					String attrValStr = (String) attrVal;
					if (attrValStr.toUpperCase().startsWith(
							driverDn.toUpperCase())) {
						return attrValStr;
					}
				}
			} else {
				LOG.info("...Detected null value in for DXML association attribute, can't modify or remove a non-existing value.");
			}
		}
		return null;
	}

	private static byte[] getUnicodeBytes(String password) {
		// Replace the "unicdodePwd" attribute with a new value
		// Password must be both Unicode and a quoted string
		String newQuotedPassword = null;
		byte[] newUnicodePassword = null;
		try {
			newQuotedPassword = "\"" + password + "\"";
			newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
		} catch (UnsupportedEncodingException e2) {
			throw new Error("UTF-16LE encoding not supported.");
		}
		return newUnicodePassword;
	}

}
